# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import logging
import threading
from concurrent.futures import ThreadPoolExecutor
from typing import List, Optional, Union

import requests

from coin.exchange.bitmex.auth import APIKeyAuthWithExpires


class BitmexRestApi(object):
  def __init__(self, api_key, api_secret, max_connections=2, logger=None):
    self._logger = logger or logging.getLogger(__name__)
    self._api_key = api_key
    self._api_secret = api_secret
    self._base_url = 'https://www.bitmex.com/api/v1'

    self._executor = ThreadPoolExecutor(max_workers=max_connections)
    self._threadlocal = threading.local()
    self._owned_sessions = []
    self._rate_limit = None  # Unknown
    self._rate_limit_remaining = None  # Unknown

  def __del__(self):
    self.close()

  def close(self):
    if self._owned_sessions is not None:
      for session in self._owned_sessions:
        session.close()
      self._owned_sessions = None

  def _new_session(self):
    self._logger.debug('New session')
    session = requests.Session()
    session.auth = APIKeyAuthWithExpires(self._api_key, self._api_secret)
    session.headers.update({'content-type': 'application/json'})
    session.headers.update({'accept': 'application/json'})
    self._owned_sessions.append(session)  # Python list.append is thread-safe.
    return session

  def get_rate_limit(self):
    return self._rate_limit

  def get_rate_limit_remaining(self):
    return self._rate_limit_remaining

  def get_wallet_history(self):
    return self._curl('/user/walletHistory', method='GET')

  def get_margin(self):
    return self._curl('/user/margin', method='GET')

  def get_position(self):
    return self._curl('/position', method='GET')

  def get_open_order(self):
    return self._curl('/order', method='GET', params={'filter': '{"open": true}'})

  def make_order_dict(self,
                      symbol: str,
                      side: str,
                      price: float,
                      qty: float,
                      order_type: Optional[str] = None,
                      exec_inst: Optional[str] = None,
                      client_order_id: Optional[str] = None,
                      text: Optional[str] = None,
                      trigger_price: Optional[float] = None,
                      trigger_by: Optional[str] = None):
    assert order_type in ('Limit', 'StopLimit')
    params = {
        'symbol': symbol,
        'side': side,
        'price': price,
        'orderQty': qty,
        'ordType': order_type or 'Limit'
    }

    if exec_inst is not None:
      params['execInst'] = exec_inst

    if client_order_id is not None:
      params['clOrdID'] = client_order_id

    if text is not None:
      params['text'] = text

    if order_type == 'StopLimit':
      assert trigger_price is not None
      params['stopPx'] = trigger_price
      params['execInst'] = trigger_by or 'LastPrice'  # 'MarkPrice'

    return params

  def create_order(self,
                   symbol: str,
                   side: str,
                   price: float,
                   qty: float,
                   order_type: Optional[str] = None,
                   exec_inst: Optional[str] = None,
                   client_order_id: Optional[str] = None,
                   text: Optional[str] = None,
                   trigger_price: Optional[float] = None):
    '''
    For detail of each parameter, refer to
      https://www.bitmex.com/api/explorer/#!/Order/Order_new
    Example:
      create_order(symbol='XBTUSD',
                   side='Buy',
                   price=8500.5,
                   qty=1000,
                   order_type='Limit'
                   exec_inst='ParticipateDoNotInitiate')
    '''
    params = self.make_order_dict(symbol=symbol,
                                  side=side,
                                  price=price,
                                  qty=qty,
                                  order_type=order_type,
                                  exec_inst=exec_inst,
                                  client_order_id=client_order_id,
                                  text=text,
                                  trigger_price=trigger_price)
    return self._curl('/order', method='POST', params=params)

  def create_bulk_order(self, order_dict_list: List):
    assert isinstance(order_dict_list, list)
    if len(order_dict_list) == 1:
      return self._curl('/order', method='POST', params=order_dict_list[0])
    else:
      return self._curl('/order/bulk', method='POST', params={'orders': order_dict_list})

  def make_amend_dict(self,
                      order_id: Optional[str] = None,
                      client_order_id: Optional[str] = None,
                      price: Optional[float] = None,
                      qty: Optional[float] = None,
                      leaves_qty: Optional[float] = None,
                      trigger_price: Optional[float] = None):
    assert (order_id is None) != (client_order_id is None)
    assert not ((qty is not None) and (leaves_qty is not None))

    params = {}

    if order_id is not None:
      params['orderID'] = order_id
    elif client_order_id is not None:
      params['origClOrdID'] = client_order_id

    if price is not None:
      params['price'] = price

    if qty is not None:
      params['orderQty'] = qty
    elif leaves_qty is not None:
      params['leavesQty'] = leaves_qty

    if trigger_price is not None:
      params['stopPx'] = trigger_price

    return params

  def amend_order(self,
                  order_id: Optional[str] = None,
                  client_order_id: Optional[str] = None,
                  price: Optional[float] = None,
                  qty: Optional[float] = None,
                  leaves_qty: Optional[float] = None):
    params = self.make_amend_dict(order_id=order_id,
                                  client_order_id=client_order_id,
                                  price=price,
                                  qty=qty,
                                  leaves_qty=leaves_qty)
    return self._curl('/order', method='PUT', params=params)

  def amend_bulk_order(self, amend_dict_list: List):
    assert isinstance(amend_dict_list, list)
    if len(amend_dict_list) == 1:
      return self._curl('/order', method='PUT', params=amend_dict_list[0])
    else:
      return self._curl('/order/bulk', method='PUT', params={'orders': amend_dict_list})

  def cancel_order(self, order_id: Optional[str] = None, client_order_id: Optional[str] = None):
    assert (order_id is None) != (client_order_id is None)
    params = {}
    if order_id is not None:
      params['orderID'] = order_id
    elif client_order_id is not None:
      params['clOrdID'] = client_order_id
    return self._curl('/order', method='DELETE', params=params)

  def cancel_bulk_order(self,
                        order_id: Optional[List[str]] = None,
                        client_order_id: Optional[List[str]] = None):
    assert (order_id is None) != (client_order_id is None)
    params = {
        'orderID': order_id,
        'clOrdID': client_order_id,
    }
    return self._curl('/order', method='DELETE', params=params)

  def cancel_all(self, *, ignore_error=False):
    return self._curl('/order/all', method='DELETE')

  def _curl(self,
            path: str,
            method: str,
            params: Optional[dict] = None,
            timeout: Union[float, int] = 7):
    return self._executor.submit(self._curl_sync, path, method, params, timeout)

  def _curl_sync(self,
                 path: str,
                 method: str,
                 params: Optional[dict] = None,
                 timeout: Union[float, int] = 7):
    url = self._base_url + path
    if method == 'GET':
      req = requests.Request(method, url, params=params)
    else:
      req = requests.Request(method, url, json=params, params=None)

    try:
      session = self._threadlocal.session
    except AttributeError:
      session = self._new_session()
      self._threadlocal.session = session

    prepared = session.prepare_request(req)
    response = session.send(prepared, timeout=timeout)
    response.raise_for_status()
    self._rate_limit = int(response.headers.get('X-RateLimit-Limit', 300))
    self._rate_limit_remaining = \
        int(response.headers.get('X-RateLimit-Remaining', 0))

    self._logger.info('Rate limit: %d / %d', self._rate_limit_remaining, self._rate_limit)
    return response.json()
